<?php
class DebugHelper
{
	static public $lang = 'zh';

	static public function debug($errorName, $debugData = null)
	{
		$debugHelperDir = dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'debug_helper' . DIRECTORY_SEPARATOR;
		if (file_exists($debugHelperDir . 'DebugFunction.php'))
		{
			include($debugHelperDir . 'DebugFunction.php');
			include($debugHelperDir . 'ErrorMessage.php');
			include($debugHelperDir . 'ErrorMessage' . ucfirst(self::$lang) . '.php');
			$debugFunctionInstance = new DebugFunction();
			if (method_exists($debugFunctionInstance, $errorName))
			{
				call_user_func(array($debugFunctionInstance, $errorName), $debugData);
			}
		}
		else
		{
			echo '<xmp>';
			print_r($debugData);
			echo '</xmp>';
		}
		trigger_error($errorName, E_USER_ERROR);
	}
}
class OpcodeCacheAdapterFactory
{
	static public function factory($opcodeCacheExtension = 'apc')
	{
		$opcodeCacheClass = 'OpcodeCacheAdapter' . ucfirst($opcodeCacheExtension);
		return extension_loaded($opcodeCacheExtension) ? new $opcodeCacheClass : null;
	}
}

class OpcodeCacheAdapterApc
{
	static public function add($cacheKey, $cacheValue)
	{
		return apc_add($cacheKey, $cacheValue);
	}

	static public function get($cacheKey)
	{
		return apc_fetch($cacheKey);
	}

	static public function delete($cacheKey)
	{
		return apc_delete($cacheKey);
	}
}

class OpcodeCacheAdapterXcache
{
	static public function add($cacheKey, $cacheValue)
	{
		return xcache_set($cacheKey, $cacheValue);
	}

	static public function get($cacheKey)
	{
		return xcache_get($cacheKey);
	}

	static public function delete($cacheKey)
	{
		return xcache_unset($cacheKey);
	}
}

class KiwiSystemCache
{
	static public $opcodeCacheInstance;

	static public function getAppOptionCacheKey($projectDir, $appName)
	{
		return 'kiwi_app_option_' . crc32($projectDir . $appName);
	}

	static public function getProjectConfigCacheKey($porjectDir)
	{
		return 'kiwi_proj_conf_' . crc32($porjectDir);
	}

	static private function _getAppOptionPath($key, $userOption, $defaultValue, $onFailed = 'set_null')
	{
		if (!isset($userOption[$key]))
		{
			if (file_exists($defaultValue))
			{
				$appOptionPath = $defaultValue;
			}
			else
			{
				if ('set_null' == $onFailed)
				{
					$appOptionPath = null;
				}
				else
				{
					DebugHelper::debug('APP_OPTION_NOT_SET', array('key' => $key, 'default_value' => $defaultValue));
				}
			}
		}
		else
		{
			if (file_exists($userOption[$key]))
			{
				$appOptionPath = is_dir($userOption[$key]) ? KiwiUtil::getPathWithDirSeparator(realpath($userOption[$key])) : realpath($userOption[$key]);
			}
			else
			{
				DebugHelper::debug('APP_OPTION_INCORRECT', array('key' => $key, 'user_value' => $userOption[$key]));
			}
		}
		return $appOptionPath;
	}

	static public function getAppOption($cacheKey, $userOption, $underMVCMode)
	{
		if ('dev' == Kiwi::$envMode || !self::$opcodeCacheInstance || !($appOptions = self::$opcodeCacheInstance->get($cacheKey)))
		{
			$appOptions = $userOption;
			$appOptions['kiwi_runtime_dir'] = KiwiUtil::getPathWithDirSeparator(dirname(__FILE__));
			if ('dev' != Kiwi::$envMode)
			{
				$appOptions['dist_dir'] = self::_getAppOptionPath('dist_dir', $userOption, $appOptions['proj_dir'] . 'dist' . DIRECTORY_SEPARATOR, 'debug');
			}
			if ($underMVCMode)
			{
				$appOptions['app_dir'] = self::_getAppOptionPath('app_dir', $userOption, $appOptions['proj_dir'] . 'app' . DIRECTORY_SEPARATOR . $appOptions['app_name'] . DIRECTORY_SEPARATOR, 'debug');
				$appOptions['app_lib_dir'] = self::_getAppOptionPath('app_lib_dir', $userOption, $appOptions['app_dir'] . 'lib' . DIRECTORY_SEPARATOR);
			}
			$appOptions['config_file'] = self::_getAppOptionPath('config_file', $userOption, $appOptions['proj_dir'] . 'conf' . DIRECTORY_SEPARATOR . 'conf.php');
			$appOptions['proj_lib_dir'] = self::_getAppOptionPath('proj_lib_dir', $userOption, $appOptions['proj_dir'] . 'lib' . DIRECTORY_SEPARATOR);
			if ('dev' != Kiwi::$envMode && self::$opcodeCacheInstance)
			{
				self::$opcodeCacheInstance->add($cacheKey, $appOptions);
			}
		}
		return $appOptions;
	}

	static public function getProjectConfig($cacheKey, $configFile)
	{
		if ('dev' == Kiwi::$envMode || !self::$opcodeCacheInstance || !($projectConfig = self::$opcodeCacheInstance->get($cacheKey)))
		{
			$defaultConfig = array('misc' => array('encoding' => 'UTF-8'),
			'route' => array('rewrite' => 'off',
				'url_pattern' => 'standard'),
			'validator' => array('error_explain' => array('ban' => ' contain banned words',
					'mask' => ' does not match the given format',
					'max_length' => ' is too long',
					'min_length' => ' is too short',
					'max_value' => ' is too big',
					'min_value' => ' is too small',
					'required' => ' is null')),
			'autoload_rule' => array()
			);
			if ($configFile)
			{
				$projectConfig = include($configFile);
				if (is_array($projectConfig))
				{
					foreach($defaultConfig as $configKey => $config)
					{
						if (isset($projectConfig[$configKey]))
						{
							$projectConfig[$configKey] = array_merge($defaultConfig[$configKey], $projectConfig[$configKey]);
						}
						else
						{
							$projectConfig[$configKey] = $defaultConfig[$configKey];
						}
					}
				}
				else
				{
					DebugHelper::debug('CONFIG_FILE_DOES_NOT_RETURN_ARRAY', array('config_file' => $configFile));
				}
			}
			else
			{
				$projectConfig = $defaultConfig;
			}
			if ('dev' != Kiwi::$envMode && self::$opcodeCacheInstance)
			{
				self::$opcodeCacheInstance->add($cacheKey, $projectConfig);
			}
		}
		return $projectConfig;
	}
}
class KiwiUtil
{
	/**
	 * Get the dir path with directory separators
	 * @param $path string
	 * @return string
	 */
	static public function getPathWithDirSeparator($path)
	{
		return '\\' == substr($path, -1) || '/' == substr($path, -1) ? $path : $path . DIRECTORY_SEPARATOR;
	}
}

class KiwiFilePacker
{
	static private function _getAutoloadRange($module, $action, $autoloadRule)
	{
		return null;
	}

	static private function _scanDir($dir)
	{
		$files = array();
		$directories = array($dir);
		while ($directories)
		{
			$currentDir = array_shift($directories);
			if ($dirHandle = opendir($currentDir))
			{
				while (false !== ($file = readdir($dirHandle)))
				{
					if ('.' != $file && '..' != $file)
					{
						$currentFile = $currentDir . $file;
						if (is_file($currentFile))
						{
							if ('.php' == substr($file, -4) && __FILE__ != $currentFile)
							{
								$files[] = $currentFile;
							}
						}
						else
						{
							$directories[] = $currentFile . DIRECTORY_SEPARATOR;
						}
					}
				}
			}
		}
		return $files;
	}

	static private function _reorderClass($child, $parent, $files, $_classes, $_extends)
	{
		$childFileIndex = array_search($_classes[$child], $files);
		$parentFileIndex = class_exists($parent, false) ? 0 : array_search($_classes[$parent], $files);
		if (false !== $childFileIndex && ($childFileIndex < $parentFileIndex || false === $parentFileIndex))
		{
			if (false !== $parentFileIndex)
			{
				unset($files[$parentFileIndex]);
			}
			for ($i = count($files); $i > $childFileIndex; $i --)
			{
				$files[$i] = $files[$i-1];
			}
			$files[$childFileIndex] = $_classes[$parent];
		}
		else
		{
			if (false === $parentFileIndex)
			{
				$files[] = $_classes[$parent];
			}
			if (false === $childFileIndex)
			{
				$files[] = $_classes[$child];
			}
		}
		if (isset($_extends[$parent]))
		{
			$child = $parent;
			$parent = $_extends[$child];
			$files = self::_reorderClass($child, $parent, $files, $_classes, $_extends);
		}
		return $files;
	}

	/**
	 * Get ordered PHP libraries
	 *
	 * @param array $paths
	 * @return void
	 */
	static private function _getOrderedFileList($paths)
	{
		$_classes = array();
		$_extends = array();
		$files = array();
		foreach ($paths as $directory)
		{
			if (is_dir($directory))
			{
				$files = array_merge($files, self::_scanDir($directory));
			}
			else if (is_file($directory))
			{
				$files[] = $directory;
			}
		}
		foreach($files as $currentFile)
		{
			$allFileList[] = $currentFile;
			$currentFileContent = trim(file_get_contents($currentFile));
			preg_match_all('~^\s*(?:abstract\s+|final\s+)?(?:class|interface)\s+(\w+)(\s+(extends|implements)\s+(\w+))?\s*{~mi', $currentFileContent, $classes);
			foreach ($classes[1] as $key => $class)
			{
				$_classes[$class] = $currentFile;
				if(isset($classes[4]) && isset($classes[4][$key]) && $classes[4][$key] )
				{
					$_extends[$class] = $classes[4][$key];
				}
			}
		}
		$files = array();
		foreach($_extends as $child => $parent)
		{
			$files = self::_reorderClass($child, $parent, $files, $_classes, $_extends);
		}
		foreach($_classes as $classFile)
		{
			if (!in_array($classFile, $files))
			{
				$files[] = $classFile;
			}
		}
		foreach($allFileList as $phpFile)
		{
			if (!in_array($phpFile, $files))
			{
				$files[] = $phpFile;
			}
		}
		return $files;
	}

	static private function _getMergedFilePath($appName, $module, $action, $autoloadRule)
	{
		$mergedFilePath = 'all_in_one.php';
		if ($module && $action)
		{
			if (isset($autoloadRule[$appName][$module][$action]))
			{
				$mergedFilePath = "$appName.$module.$action.php";
			}
			else if (null !== $appName)
			{
				$mergedFilePath = "$appName.php";
			}
		}
		return $mergedFilePath;
	}

	/*
	 * Load library files, if run in production environment, merge all library files to one big file, and then load it.
	 *
	 * @param array $appOptions
	 * @param string $module
	 * @param string $action
	 * @param array $autoloadRule
	 * @param boolean $executeInclulde If "$executeInclulde" is set to false, this function only merges file, but don't include it. This switch is used for deploy tool.
	 * @return void
	 * @todo dont't parse autoload_rule if in production env and merged_file exists
	 */
	static public function loadFile($appOptions = array(), $module = null, $action = null, $autoloadRule = array(), $executeInclulde = true)
	{
		if ('dev' != Kiwi::$envMode)
		{
			$mergedFilePath = $appOptions['dist_dir'];
			$mergedFilePath .= self::_getMergedFilePath($appOptions['app_name'], $module, $action, $autoloadRule);
		}
		if (isset($mergedFilePath) && file_exists($mergedFilePath) && $executeInclulde)
		{
			include($mergedFilePath);
		}
		else
		{
			$appOptions['kiwi_runtime_dir'] = isset($appOptions['kiwi_runtime_dir']) ? $appOptions['kiwi_runtime_dir'] : KiwiUtil::getPathWithDirSeparator(dirname(__FILE__));
			if ($module && $action && isset($autoloadRule[$appOptions['app_name']][$module][$action]))
			{
				$autoloadRange = self::_getAutoloadRange($module, $action, $autoloadRule);
			}
			else
			{
				$autoloadRange = array($appOptions['kiwi_runtime_dir']);
				if (isset($appOptions['proj_lib_dir']) && is_dir($appOptions['proj_lib_dir']))
				{
					$autoloadRange[] = $appOptions['proj_lib_dir'];
				}
				if (isset($appOptions['app_lib_dir']) && is_dir($appOptions['app_lib_dir']))
				{
					$autoloadRange[] = $appOptions['app_lib_dir'];
				}
			}
			$orderedFiles = self::_getOrderedFileList($autoloadRange);
			if ('dev' != Kiwi::$envMode)
			{
				$mergedSwapFilePath = "$mergedFilePath.swap";
				if (!file_exists($mergedSwapFilePath))
				{
					$mergedSwapFileHandle = fopen($mergedSwapFilePath, 'w');
				}
				$mergedFileContent = '<?php';
				foreach($orderedFiles as $file)
				{
					if ($executeInclulde)
					{
						include($file);
					}
					if (isset($mergedSwapFileHandle))
					{
						$currentFileContent = trim(file_get_contents($file));
						$currentFileContent = substr($currentFileContent, 5);
						if ('?>' == substr($currentFileContent, -2))
						{
							$currentFileContent = substr($currentFileContent, 0, -2);
						}
						$mergedFileContent .= $currentFileContent;
					}
				}
				if (isset($mergedSwapFileHandle))
				{
					fwrite($mergedSwapFileHandle, $mergedFileContent);
					fclose($mergedSwapFileHandle);
					copy($mergedSwapFilePath, $mergedFilePath);
					unlink($mergedSwapFilePath);
				}
			}
			else
			{
				foreach($orderedFiles as $file)
				{
					include($file);
				}
			}
		}
	}

}
/**
 * Kiwi class
 */
class Kiwi
{
	static public $applicationDir;

	static public $envMode = 'dev';
	/**
	 * Run kiwiphp app
	 *
	 * @param array $appOptions
	 * @param string $module
	 * @param string $action
	 */
	static public function run($appOptions = null, $module = null, $action = null)
	{
		/**
		 * Get module and action
		 */
		$uri = self::route();
		if (isset($uri['module']))
		{
			$module = $uri['module'];
		}
		if (isset($uri['action']))
		{
			$action = $uri['action'];
		}
		$underMVCMode = $module && $action ? true : false;

		KiwiSystemCache::$opcodeCacheInstance = isset($appOptions['opcode_cache_ext']) ? OpcodeCacheAdapterFactory::factory($appOptions['opcode_cache_ext']) : OpcodeCacheAdapterFactory::factory();

		/**
		 * Get cached configuration from opcode cache
		 */
		if (!isset($appOptions['proj_dir']) || !isset($appOptions['app_name']))
		{
			$includedFiles = get_included_files();
			$appBootStrap = $includedFiles[0];
		}
		$appOptions['proj_dir'] = isset($appOptions['proj_dir']) ? KiwiUtil::getPathWithDirSeparator($appOptions['proj_dir']) : KiwiUtil::getPathWithDirSeparator(dirname(dirname(dirname($appBootStrap))));
		$appOptions['app_name'] = isset($appOptions['app_name']) ? $appOptions['app_name'] : basename($appBootStrap, '.php');
		$AppOptionCacheKey = KiwiSystemCache::getAppOptionCacheKey($appOptions['proj_dir'], $appOptions['app_name']);
		$appOptions = KiwiSystemCache::getAppOption($AppOptionCacheKey, $appOptions, $underMVCMode);

		/**
		 * Init project configutation
		 */
		$projectConfigCacheKey = KiwiSystemCache::getProjectConfigCacheKey($appOptions['proj_dir']);
		$projectConfig = KiwiSystemCache::getProjectConfig($projectConfigCacheKey, $appOptions['config_file']);

		KiwiFilePacker::loadFile($appOptions, $module, $action, $projectConfig['autoload_rule']);
		Config::$array = $projectConfig;

		/**
		 * Init database configuration
		 */
		if (isset(Config::$array['db_server']))
		{
			Db::$server = Config::$array['db_server'];
			Db::setDefaultConfig(array('charset' => Config::$array['misc']['encoding']));
		}
		if (isset(Config::$array['db_table']))
		{
			Db::$table = Config::$array['db_table'];
		}

		/**
		 * Set the internal encoding
		 */
		if(extension_loaded("mbstring"))
		{
			mb_internal_encoding(Config::$array['misc']['encoding']);
		}

		if($underMVCMode)
		{
			self::$applicationDir = $appOptions['app_dir'];
			Dispatcher::dispatchAction($module, $action);
		}
	}

	/**
	 * Set envorinment mode
	 * @param string $envMode
	 */
	static public function setEnvMode($envMode)
	{
		self::$envMode = $envMode;
	}

	/**
	 * Route the user request.
	 */
	static public function route()
	{
		$uri = array();
		if (isset($_SERVER['SERVER_PROTOCOL']))
		{
			if (isset($_REQUEST['module']))
			{
				$uri['module'] = $_REQUEST['module'];
			}
			if (isset($_REQUEST['action']))
			{
				$uri['action'] = $_REQUEST['action'];
			}
		}
		else
		{
			$i = 0;
			while ((!isset($uri['module']) || !isset($uri['action'])) && isset($_SERVER['argv'][$i]))
			{
				if (("-m" == $_SERVER['argv'][$i] || "--module" == $_SERVER['argv'][$i]) && isset($_SERVER['argv'][$i+1]))
				{
					$uri['module'] = $_SERVER['argv'][$i+1];
				}
				else if (("-a" == $_SERVER['argv'][$i] || "--action" == $_SERVER['argv'][$i]) && isset($_SERVER['argv'][$i+1]))
				{
					$uri['action'] = $_SERVER['argv'][$i+1];
				}
				$i ++;
			}
		}
		if (isset($uri['module']) && !preg_match('/^[a-zA-Z0-9\.\-_]+$/', $uri['module']))
		{
			if (function_exists('onModuleNameIllegal'))
			{
				call_user_func('onModuleNameIllegal', $uri['module']);
			}
			else
			{
				throw new Exception("Module name is illegal: {$uri['module']}");
			}
		}
		if (isset($uri['action']) && !preg_match('/^[a-zA-Z0-9\.\-_]+$/', $uri['action']))
		{
			if (function_exists('onaActionNameIllegal'))
			{
				call_user_func('onaActionNameIllegal', $uri['action']);
			}
			else
			{
				throw new Exception("Action name is illegal: {$uri['action']}");
			}
		}
		return $uri;
	}
}
